
namespace Patterns4Net.Core.CecilExtensions
{
    using System;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using Patterns4Net.Attributes;
    using Patterns4Net.Attributes.DesignPatterns;

    [Immutable]
    public sealed class StackItem
    {
        private readonly MethodDefinition method;

        private readonly State state;

        /// <summary>
        /// Initializes a new instance of the <see cref="StackItem"/> class.
        /// </summary>
        /// <param name="method">The method in which body the instruction was found.</param>
        /// <param name="currentStackState">The current stack state. Some functionality of <see cref="StackItem"/> 
        /// requires this information.</param>
        /// <param name="pushInstruction">The instruction which pushes this item onto the stack.</param>
        /// <param name="operandNum">The number of the operand of the instruction.
        /// Some instructions push more than one item onto the stack.
        /// Operands are numbered from zero.</param>
        internal StackItem(
            MethodDefinition method, 
            StackState currentStackState,
            Instruction pushInstruction, 
            int operandNum = 0)
        {
            this.method = method;
            Contract.Requires(pushInstruction != null);
            Contract.Ensures(this.PushInstruction == pushInstruction);
            Contract.Ensures(this.method == method);
            Contract.Ensures(this.state != null);
            Contract.Ensures(this.OperandNum == operandNum);

            this.PushInstruction = pushInstruction;
            this.OperandNum = operandNum;

            if (pushInstruction.NormalizedOpCode() == OpCodes.Ldarg)
            {
                this.state = new ParameterPush(this);
            }
            else if (pushInstruction.NormalizedOpCode() == OpCodes.Ldloc)
            {
                this.state = new LocalVariable(this);
            }
            else if (pushInstruction.IsCallInstruction())
            {
                this.state = new MethodReturnValue(this);
            }
            else if (pushInstruction.OpCode == OpCodes.Newobj)
            {
                this.state = new NewObj(this);
            }
            else if (pushInstruction.OpCode == OpCodes.Ldfld || pushInstruction.OpCode == OpCodes.Ldsfld)
            {
                this.state = new FieldPush(this, currentStackState);
            }
            else
            {
                this.state = new Other(this);
            }
        }

        /// <summary>
        /// Gets the instruction that caused this item to be pushed onto the stack.
        /// </summary>
        public Instruction PushInstruction { get; private set; }

        /// <summary>
        /// Gets the number of the operand of the instruction.
        /// Some instructions push more than one item onto the stack.
        /// Operands are numbered from zero.
        /// </summary>
        public int OperandNum { get; private set; }

        public bool IsMethodReturnValue
        {
            get { return this.state.IsMethodReturnValue; }
        }

        public bool IsThisParameter
        {
            get { return this.state.IsThis; }
        }

        public bool IsParameter
        {
            get { return this.state.IsParameter; }
        }

        /// <summary>
        /// This the same index as Cecil uses. 
        /// It means indexes start from zero and 'this' parameter (if any) is not counted.
        /// </summary>
        public int ParameterIndex
        {
            get { return this.state.ParameterIndex; }
        }

        public bool IsLocalVar
        {
            get { return this.state.IsLocalVar; }
        }

        public int LocalVarIndex
        {
            get { return this.state.LocalVarIndex; }
        }

        public bool IsNewObject
        {
            get { return this.state.IsNewObject; }
        }

        public bool IsField
        {
            get { return this.state.IsField; }
        }

        public FieldReference FieldReference
        {
            get { return this.state.FieldReference; }
        }

        public StackItem FieldOwner
        {
            get { return this.state.FieldOwner; }
        }

        public TypeReference ItemType
        {
            get { return this.state.ItemType; }
        }

        /// <summary>
        /// Values of properties like <see cref="StackItem.IsThisParameter"/> depends on the 
        /// push instruction type and operand. This abstract class represents the 
        /// strategy for calculating these values in dependence on the instruction.
        /// </summary>
        private abstract class State
        {            
            protected State(StackItem parent)
            {
                this.Parent = parent;
            }

            public virtual bool IsMethodReturnValue
            {
                get { return false; }
            }

            public virtual bool IsThis
            {
                get { return false; }
            }

            public virtual bool IsParameter
            {
                get { return false; }
            }

            public virtual bool IsLocalVar
            {
                get { return false; }
            }

            public virtual bool IsNewObject
            {
                get { return false; }
            }

            public virtual bool IsField
            {
                get { return false; }
            }

            public virtual TypeReference ItemType
            {
                get { return null; }
            }

            public virtual int LocalVarIndex
            {
                get
                {
                    throw new InvalidOperationException("This operation is only valid if the item has IsLovalVar = true.");
                }
            }

            protected StackItem Parent { get; private set; }

            public virtual int ParameterIndex
            {
                get
                {
                    throw new InvalidOperationException("This operation is only valid if the item has IsParameter = true.");
                }
            }

            public virtual FieldReference FieldReference
            {
                get
                {
                    throw new InvalidOperationException("This operation is only valid if the item has IsField = true.");
                }
            }

            public virtual StackItem FieldOwner
            {
                get
                {
                    throw new InvalidOperationException("This operation is only valid if the item has IsField = true.");
                }
            }
        }

        private class Other : State
        {
            public Other(StackItem parent)
                : base(parent)
            {
            }
        }

        private class NewObj : State
        {
            public NewObj(StackItem parent)
                : base(parent)
            {
            }

            public override bool IsNewObject
            {
                get { return true; }
            }

            public override TypeReference ItemType
            {
                get
                {
                    return ((MethodReference)this.Parent.PushInstruction.Operand).DeclaringType;
                }
            }
        }

        private class LocalVariable : State
        {
            private readonly Instruction pushInstruction;

            public LocalVariable(StackItem parent)
                : base(parent)
            {
                this.pushInstruction = parent.PushInstruction.Normalize(parent.method.Body);
            }

            public override bool IsLocalVar
            {
                get { return true; }
            }

            public override int LocalVarIndex
            {
                get
                {
                    return ((VariableReference)this.pushInstruction.Operand).Index;
                }
            }

            public override TypeReference ItemType
            {
                get
                {
                    return ((VariableReference)this.pushInstruction.Operand).VariableType;
                }
            }
        }

        private class MethodReturnValue : State
        {
            public MethodReturnValue(StackItem parent)
                : base(parent)
            {
                Contract.Requires(parent.PushInstruction.Operand is MethodReference);
            }

            public override bool IsMethodReturnValue
            {
                get { return true; }
            }

            public override TypeReference ItemType
            {
                get
                {
                    return ((MethodReference)this.Parent.PushInstruction.Operand).ReturnType;
                }
            }
        }

        private class ParameterPush : State
        {
            private readonly Instruction pushInstruction;

            public ParameterPush(StackItem parent)
                : base(parent)
            {
                Contract.Ensures(this.pushInstruction.Operand is ParameterDefinition);
                this.pushInstruction = parent.PushInstruction.Normalize(parent.method.Body);
            }

            public override bool IsParameter
            {
                get { return true; }
            }

            public override bool IsThis
            {
                get
                {
                    return this.Parent.method.Body.ThisParameter == this.pushInstruction.Operand;
                }
            }

            public override int ParameterIndex
            {
                get
                {
                    return ((ParameterDefinition)this.pushInstruction.Operand).Index;
                }
            }

            public override TypeReference ItemType
            {
                get
                {
                    return ((ParameterDefinition)this.pushInstruction.Operand).ParameterType;
                }
            }

            [ContractInvariantMethod]
            private void ObjectInvariant()
            {
                Contract.Invariant(this.pushInstruction.Operand is ParameterDefinition);
            }
        }

        private class FieldPush : State
        {
            private readonly StackItem fieldOwner;

            public FieldPush(StackItem parent, StackState stackState)
                : base(parent)
            {
                // static fields does not have the owner
                if (parent.PushInstruction.OpCode != OpCodes.Ldsfld)
                {
                    this.fieldOwner = stackState.Last();
                }
            }

            public override bool IsField
            {
                get { return true; }
            }

            public override FieldReference FieldReference
            {
                get { return (FieldReference)this.Parent.PushInstruction.Operand; }
            }

            public override StackItem FieldOwner
            {
                get
                {
                    return this.fieldOwner;
                }
            }
        }
    }
}
